package com.duomi.lang;

import java.util.ArrayList;
import java.util.List;

public class Lexer {

    @SuppressWarnings("WeakerAccess")
    public static class Token {
        public final TokenType type;
        public final Object value;

        String source;
        int column;
        int row;

        public Token(TokenType type, Object value) {
            this.type = type;
            this.value = value;
        }

        public <T> T getValue() {
            //noinspection unchecked
            return ((T) value);
        }

        @Override
        public String toString() {
            return "Token{" +
                    "type=" + type +
                    ", value=" + value +
                    ", row=" + row +
                    ", column=" + column +
                    '}';
        }
    }

    public enum TokenType {
        BOOL,
        INT,
        DOUBLE,
        STRING,
        IDENTITY,
        KEYWORD,
        OPEN_BRACKET,
        CLOSE_BRACKET,
        OPEN_BLOCK,
        CLOSE_BLOCK,
        DOT,
    }

    private final String source;
    private final List<Token> tokens = new ArrayList<>();

    private int index;
    private boolean hasLex = false;

    private int column = 1;
    private int row = 1;


    Lexer(String source) {
        this.source = source;
    }

    List<Token> getTokens() {
        if (!hasLex) {
            lex();
            hasLex = true;
        }
        return tokens;
    }

    private String advance(int step) {
        final int start = this.index;
        for (int i = 0; i < step; i++) {
            final char c = peek();
            if (c == '\n') {
                row += 1;
                column = 1;
            } else {
                column += 1;
            }
            this.index += 1;
        }

        return source.substring(start, index);
    }

    @SuppressWarnings("SameParameterValue")
    private void advanceWithoutCapture(int step) {
        this.index += step;
    }

    private char lookAhead(int step) {
        return source.charAt(index + step);
    }

    private char peek() {
        return source.charAt(index);
    }

    private boolean isEof(int step) {
        return index + step >= source.length();
    }

    private int tryAdvance(Predicate<Character> predicate) {
        int step = 0;
        while (!isEof(step) && predicate.test(lookAhead(step))) {
            step++;
        }
        return step;
    }

    private String advance(int minCount, String desc, Predicate<Character> predicate) {
        int step = 0;
        while (!isEof(step) && predicate.test(lookAhead(step))) {
            step++;
        }
        if (step >= minCount) {
            return advance(step);
        }
        throw new SyntaxException("except at least: " + minCount + " times with: " + desc + ", but get: " + step + " times");
    }

    private void addToken(TokenType type, Object value) {
        final Token tok = new Token(type, value);
        tok.column = column;
        tok.row = row;
        tok.source = source;
        tokens.add(tok);
    }

    private void lex() {
        while (!isEof(0)) {
            try {
                parseChar();
            } catch (Exception e) {
                if (e instanceof SyntaxException) {
                    throw e;
                } else {
                    throw new SyntaxException("lex failed", e);
                }
            }
        }
    }

    private void parseChar() {
        final char c = peek();
        if (isSpaceChar(c)) {
            advanceWithoutCapture(1);
            return;
        }

        switch (c) {
            case '#':
                parseComment();
                break;
            case '{':
                addToken(TokenType.OPEN_BLOCK, advance(1));
                break;
            case '(':
                addToken(TokenType.OPEN_BRACKET, advance(1));
                break;
            case '}':
                addToken(TokenType.CLOSE_BLOCK, advance(1));
                break;
            case ')':
                addToken(TokenType.CLOSE_BRACKET, advance(1));
                break;
            case '0':
            case '1':
            case '2':
            case '3':
            case '4':
            case '5':
            case '6':
            case '7':
            case '8':
            case '9':
                parseDigit();
                break;
            case '"':
                parseString();
                break;
            case '.':
                parseDot();
                break;
            default:
                parseAsIdentity();
                break;
        }
    }

    private boolean isSpaceChar(char c) {
        return Character.isSpaceChar(c) || c == '\n';
    }

    private void parseDot() {
        advance(1);

        final String id = advance(1, "NotIdentityBoundary", this::isIdentityBoundary);
        addToken(TokenType.DOT, id);
    }

    private void parseAsIdentity() {

        final String id = advance(1, "NotIdentityBoundary", this::isIdentityBoundary);

        if (id.equals("true") || id.equals("false")) {
            addToken(TokenType.BOOL, Boolean.parseBoolean(id));
            return;
        }

        if (Constants.Keywords.contains(id)) {
            addToken(TokenType.KEYWORD, id);
            return;
        }

        addToken(TokenType.IDENTITY, id);
    }

    private boolean isIdentityBoundary(Character c) {
        return !Character.isSpaceChar(c) && "([{}])".indexOf(c) < 0;
    }

    private void parseString() {

        advanceWithoutCapture(1); // consume open "

        int step = 0;
        while (true) {
            if (isEof(step)) {
                throw new SyntaxException("unclosed string");
            }

            final char c = lookAhead(step);
            final char last = lookAhead(step - 1);

            if (c == '"' && last != '\\') {
                break;
            }
            step += 1;
        }

        final String content = advance(step);
        advanceWithoutCapture(1); // consume close "
        addToken(TokenType.STRING, content);
    }

    private void parseDigit() {
        final String intPart = advance(1, "Digit", Character::isDigit);
        if (tryAdvance(isChar('.')) > 0) {
            advanceWithoutCapture(1);
            final String decimalPart = advance(1, "Digit", Character::isDigit);
            addToken(TokenType.DOUBLE, Double.parseDouble(intPart + "." + decimalPart));
        } else {
            addToken(TokenType.INT, Integer.parseInt(intPart));
        }
    }

    private static Predicate<Character> isChar(char c) {
        return test -> test == c;
    }

    private void parseComment() {
        advance(0, "Comment", c -> c != '\n');
    }
}
